package fr.limsi.seven;

import java.awt.Color;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.Timer;

/**
 * @author Camille
 */
public class Pyramide2DAnimator {

	/**
	 * Animation callback delay in milliseconds
	 */
	public static final int ANIMATION_DELAY = 20;

	/**
	 * Animation length image count
	 */
	public static final int ANIMATION_LENGTH = 150;

	/**
	 * The {@link Pyramide2D} to animate
	 */
	protected Pyramide2D pyramide2D;

	/**
	 * Timer for translation transformation
	 */
	protected Timer translationTimer;

	/**
	 * Timer for scaling transformation
	 */
	protected Timer scaleTimer;

	/**
	 * Timer for color transformation
	 */
	protected Timer colorTimer;

	/**
	 * Constructs a <i>Viewer2DAnimator</i>
	 * 
	 * @param view
	 *            the {@link Pyramide2D} to animate
	 */
	public Pyramide2DAnimator(Pyramide2D view) {
		this.pyramide2D = view;
	}

	/**
	 * Creates the translation {@link Animation}
	 * 
	 * @param to
	 *            the translation goal point
	 * @return the created {@link Animation}
	 */
	public Animation getTranslationAnimation(Point to) {

		final Point goal = to;

		Thread anim = new Thread() {
			@Override
			public void run() {

				translate(goal);

				waitForTranslationTimer();

				pyramide2D.getAnimationMonitor().endReachedBy(this);
			}
		};

		anim.setName("Translation");

		return new Animation(Animation.TRANSLATION_ANIM, anim);
	}

	/**
	 * Creates the scale {@link Animation}
	 * 
	 * @param scale
	 *            the goal scale
	 * @return the created {@link Animation}
	 */
	public Animation getScaleAnimation(float scale) {

		final float goal = scale;

		Thread anim = new Thread() {
			@Override
			public void run() {

				scale(goal);

				waitForScaleTimer();

				pyramide2D.getAnimationMonitor().endReachedBy(this);
			}
		};

		anim.setName("Scale");

		return new Animation(Animation.SCALE_ANIM, anim);
	}

	/**
	 * Creates the color {@link Animation}
	 * 
	 * @param to
	 *            the goal color
	 * @return the created {@link Animation}
	 */
	public Animation getColorAnimation(Color to) {

		final Color goal = to;

		Thread anim = new Thread() {
			@Override
			public void run() {

				changeColor(goal);

				waitForColorTimer();

				pyramide2D.getAnimationMonitor().endReachedBy(this);
			}
		};

		anim.setName("Color");

		return new Animation(Animation.COLOR_ANIM, anim);
	}

	/**
	 * Translates the current view to a given position
	 * 
	 * @param to
	 *            the translation goal point
	 */
	public void translate(Point to) {

		stopTranslationTimer();

		final Point from = pyramide2D.getTranslation();
		final Point goal = to;

		// Step calculation
		float dx = (to.x - from.x) / (ANIMATION_LENGTH / ANIMATION_DELAY);
		float dy = (to.y - from.y) / (ANIMATION_LENGTH / ANIMATION_DELAY);

		// dx step correction (or else whe never stop the currentAnimation)
		if (dx < 0 && dx > -1) {
			dx = -1;
		}

		if (dx > 0 && dx < 1) {
			dx = 1;
		}

		// dy step correction (or else whe never stop the currentAnimation)
		if (dy < 0 && dy > -1) {
			dy = -1;
		}

		if (dy > 0 && dy < 1) {
			dy = 1;
		}

		final float deltaX = dx;
		final float deltaY = dy;
		final boolean increaseX = (to.x > from.x);
		final boolean increaseY = (to.y > from.y);

		ActionListener translateCallback = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				translationEffect(goal, deltaX, increaseX, deltaY, increaseY);
			}
		};

		translationTimer = new Timer(0, translateCallback);
		translationTimer.setDelay(ANIMATION_DELAY);
		translationTimer.start();
		pyramide2D.setTranslating(true);
	}

	/**
	 * Scales the current view
	 * 
	 * @param scale
	 *            the scaling factor
	 */
	public void scale(float scale) {

		stopScaleTimer();

		final float from = pyramide2D.getScale();
		final float goal = scale;

		float d = (goal - from) / (ANIMATION_LENGTH / ANIMATION_DELAY);

		final float delta = d;
		final boolean enlarge = (goal > from);

		ActionListener scaleCallback = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				scaleEffect(goal, delta, enlarge);
			}
		};

		scaleTimer = new Timer(0, scaleCallback);
		scaleTimer.setDelay(ANIMATION_DELAY);
		scaleTimer.start();
		pyramide2D.setScaling(true);

	}

	/**
	 * Changes the viewer current drawing color
	 * 
	 * @param scale
	 *            the scaling factor
	 */
	public void changeColor(Color to) {

		stopColorTimer();

		final Color from = pyramide2D.getDrawingColor();
		final Color goal = to;

		float dR = ((to.getRed() - from.getRed()) / (ANIMATION_LENGTH / ANIMATION_DELAY));
		float dG = ((to.getGreen() - from.getGreen()) / (ANIMATION_LENGTH / ANIMATION_DELAY));
		float dB = ((to.getBlue() - from.getBlue()) / (ANIMATION_LENGTH / ANIMATION_DELAY));
		float dA = ((to.getAlpha() - from.getAlpha()) / (ANIMATION_LENGTH / ANIMATION_DELAY));

		// Red composite step correction
		if (dR < 0 && dR > -1) {
			dR = -1;
		}

		if (dR > 0 && dR < 1) {
			dR = 1;
		}

		// Green composite step correction
		if (dG < 0 && dG > -1) {
			dG = -1;
		}

		if (dG > 0 && dG < 1) {
			dG = 1;
		}

		// Blue composite step correction
		if (dB < 0 && dB > -1) {
			dB = -1;
		}

		if (dB > 0 && dB < 1) {
			dB = 1;
		}

		// Alpha composite step correction
		if (dA < 0 && dA > -1) {
			dA = -1;
		}

		if (dA > 0 && dA < 1) {
			dA = 1;
		}

		final float deltaR = dR;
		final float deltaG = dG;
		final float deltaB = dB;
		final float deltaA = dA;
		final float aDr = Math.abs(dR);
		final float aDg = Math.abs(dG);
		final float aDb = Math.abs(dB);
		final float aDa = Math.abs(dA);

		ActionListener colorCallback = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				colorEffect(goal, deltaR, deltaG, deltaB, deltaA, aDr, aDg, aDb, aDa);
			}
		};

		colorTimer = new Timer(0, colorCallback);
		colorTimer.setDelay(ANIMATION_DELAY);
		colorTimer.start();
	}

	/**
	 * Callback function for <i>translate()</i>
	 * 
	 * @param to
	 *            aimed translation
	 * @param dx
	 *            interpolation step for translation x coordinate
	 * @param increaseX
	 *            true if x coordinate must be increased, or else false
	 * @param dy
	 *            interpolation step for translation y coordinate
	 * @param increaseY
	 *            true if y coordinate must be increased, or else false
	 */
	protected void translationEffect(Point to, float dx, boolean increaseX, float dy, boolean increaseY) {

		int x = pyramide2D.getTranslation().x;
		int y = pyramide2D.getTranslation().y;

		// Interpolation
		if ((increaseX && (x + dx <= to.x + 1)) || (!increaseX && (x + dx >= to.x - 1))) {
			x += dx;
		}
		else {
			if (x != to.x) {
				x += to.x - x;
			}
		}

		if ((increaseY && (y + dy <= to.y + 1)) || (!increaseY && (y + dy >= to.y - 1))) {
			y += dy;
		}
		else {
			if (y != to.y) {
				y += to.y - y;
			}
		}

		// Stop condition
		if ((dx == 0 && dy == 0) || ((to.x == x) && (to.y == y))) {
			stopTranslationTimer();
			pyramide2D.setTranslating(false);
		}

		pyramide2D.setTranslation(new Point(x, y));
		pyramide2D.positionSecondaryObjects();

		pyramide2D.repaint();
	}

	/**
	 * Callback function for <i>scale()</i>
	 * 
	 * @param to
	 *            aimed scale
	 * @param delta
	 *            scale interpolation step
	 * @param enlarge
	 *            true if the scale effect is a zoom in, or else false
	 */
	protected void scaleEffect(float to, float delta, boolean enlarge) {

		// Interpolation
		float scale = pyramide2D.getScale();

		if ((enlarge && (scale + delta <= to)) || (!enlarge && (scale + delta >= to))) {
			scale += delta;
		}
		else {
			if (scale != to) {
				scale += (to - scale);
			}
		}

		// Stop condition
		if ((delta == 0) || (to == scale)) {
			stopScaleTimer();
			pyramide2D.setScaling(false);
		}

		pyramide2D.setScale(scale);
		pyramide2D.positionSecondaryObjects();

		pyramide2D.repaint();
	}

	/**
	 * Callback function for <i>changeColor()</i>
	 * 
	 * @param to
	 *            aimed color
	 * @param dR
	 *            red composite interpolation step
	 * @param dG
	 *            green composite interpolation step
	 * @param dB
	 *            blue composite interpolation step
	 * @param dA
	 *            alpha composite interpolation step
	 * @param aDr
	 *            dR absolute value
	 * @param aDg
	 *            dG absolute value
	 * @param aDb
	 *            dB absolute value
	 * @param aDa
	 *            dA absolute value
	 */
	protected void colorEffect(Color to, float dR, float dG, float dB, float dA, float aDr, float aDg, float aDb, float aDa) {

		int r = pyramide2D.getDrawingColor().getRed();
		int g = pyramide2D.getDrawingColor().getGreen();
		int b = pyramide2D.getDrawingColor().getBlue();
		int a = pyramide2D.getDrawingColor().getAlpha();

		// Interpolation
		if (!(to.getRed() - aDr <= r && to.getRed() + aDr >= r)) {
			r += dR;
		}

		if (!(to.getGreen() - aDg <= g && to.getGreen() + aDg >= g)) {
			g += dG;
		}

		if (!(to.getBlue() - aDb <= b && to.getBlue() + aDb >= b)) {
			b += dB;
		}

		if (!(to.getAlpha() - aDa <= a && to.getAlpha() + aDa >= a)) {
			a += dA;
		}

		// Stop condition
		if ((dR == 0 && dG == 0 && dB == 0 && dA == 0) || ((to.getRed() - aDr <= r && to.getRed() + aDr >= r) && (to.getGreen() - aDg <= g && to.getGreen() + aDg >= g) && (to.getBlue() - aDb <= b && to.getBlue() + aDb >= b) && (to.getAlpha() - aDa <= a && to.getAlpha() + aDa >= a))) {
			stopColorTimer();
		}
		else {
			pyramide2D.setDrawingColor(new Color(r, g, b, a));
		}

		pyramide2D.repaint();
	}

	/**
	 * Stops the <i>translationTimer</i>
	 */
	public synchronized void stopTranslationTimer() {
		if (translationTimer != null) {
			translationTimer.stop();
		}

		notify();
	}

	/**
	 * Waits for the <i>translationTimer</i> to stop
	 */
	public synchronized void waitForTranslationTimer() {
		while (translationTimer != null && translationTimer.isRunning()) {
			try {
				wait();
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Stops the <i>scaleTimer</i>
	 */
	public synchronized void stopScaleTimer() {
		if (scaleTimer != null) {
			scaleTimer.stop();
		}

		notify();
	}

	/**
	 * Waits for the <i>scaleTimer</i> to stop
	 */
	public synchronized void waitForScaleTimer() {
		while (scaleTimer != null && scaleTimer.isRunning()) {
			try {
				wait();
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Stops the <i>colorTimer</i>
	 */
	public synchronized void stopColorTimer() {
		if (colorTimer != null) {
			colorTimer.stop();
		}

		notify();
	}

	/**
	 * Waits for the <i>colorTimer</i> to stop
	 */
	public synchronized void waitForColorTimer() {
		while (colorTimer != null && colorTimer.isRunning()) {
			try {
				wait();
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
